spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import { WatchButton } from "@/components/intelligence/watch-button";2import type { EventType, ProjectDetail } from "@dci/core";3import { partialDateSortKey } from "@dci/core";4import type { Metadata } from "next";5import Link from "next/link";6import { notFound } from "next/navigation";7import type { ReactNode } from "react";8import { Breadcrumbs } from "@/components/entity/breadcrumbs";9import { EntityMap } from "@/components/entity/entity-map";10import { JsonLd } from "@/components/entity/json-ld";11import { ExtLink, KeyValueList } from "@/components/entity/kv";12import { ProvenanceTable, WithProvenance } from "@/components/entity/provenance";13import { SectionNav } from "@/components/entity/section-nav";14import { SourcesFooter } from "@/components/entity/sources-footer";15import { EventList } from "@/components/event-list";16import { ClaimsTable, DataQualityPanel, Evidence, EvidenceProvider, NearbyInfrastructurePanel } from "@/components/evidence";17import { ProjectStages, ProjectVelocity } from "@/components/project-stages";18import { AiMark, Badge, ConfidenceBadge, EstimateMark, PrecisionBadge, ScopeBadge, SemanticsLabel, StatusBadge } from "@/components/ui/badge";19import { Page, Section } from "@/components/ui/card";20import { CountryFlag } from "@/components/ui/country-flag";21import { ErrorState, OfflineNotice } from "@/components/ui/states";22import { SourceHistory, TimelineList, type TimelineEntry } from "@/components/ui/timeline";23import { countryName, fmtDate, fmtInt, fmtMw, fmtUsd, placeLine, splitMw } from "@/lib/format";24import { fmtPartialDateSafe } from "@/lib/format-project";25import { CONFIDENCE_LABEL, EVENT_LABEL, PRECISION_LABEL, STATUS_LABEL, isSiteScopeLabel } from "@/lib/labels";26import { routes } from "@/lib/routes";27import { absoluteUrl, compactJsonLd, NOT_FOUND_METADATA, pageMetadata } from "@/lib/seo";28import { getProject } from "./data";2930export const revalidate = 300;3132type Props = { params: Promise<{ slug: string }> };3334function describe(p: ProjectDetail): string {35 if (p.description) return p.description;36 const bits = [`${p.name}: ${STATUS_LABEL[p.status].toLowerCase()} data center project`];37 if (p.operator) bits.push(`by ${p.operator.name}`);38 const place = placeLine(p);39 if (place !== "—") bits.push(`in ${place}`);40 if (p.plannedMw !== null && siteScoped(p.capacityScope)) bits.push(`— ≈ ${fmtMw(p.plannedMw)} planned (published figure)`);41 if (p.investmentUsd !== null && siteScoped(p.investmentScope)) bits.push(`, ${fmtUsd(p.investmentUsd)} announced`);42 if (p.expectedOpening) bits.push(`, expected ${fmtPartialDateSafe(p.expectedOpening)}`);43 return `${bits.join(" ")}. Lifecycle stages, timeline, claims with scope and provenance.`;44}4546/** A header figure may only stand for the project when its scope is a site scope (or unknown-but-assigned legacy). */47function siteScoped(scope: string | null | undefined): boolean {48 return scope === null || scope === undefined || isSiteScopeLabel(scope);49}5051export async function generateMetadata({ params }: Props): Promise<Metadata> {52 const { slug } = await params;53 const r = await getProject(slug);54 if (!r.ok) return r.status === 404 ? NOT_FOUND_METADATA : { title: "Project" };55 return pageMetadata({ title: r.data.name, description: describe(r.data), path: routes.project(r.data.slug) });56}5758function humanType(t: string): string {59 return EVENT_LABEL[t as EventType] ?? t.replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());60}6162function toneFor(t: string): TimelineEntry["tone"] {63 switch (t) {64 case "construction_started":65 case "phase_announced":66 return "warn";67 case "facility_opened":68 case "cloud_region_launched":69 return "ok";70 case "project_announced":71 case "expansion_announced":72 case "investment_announced":73 case "power_agreement":74 return "accent";75 case "closure":76 case "incident":77 case "project_cancelled":78 return "danger";79 default:80 return "neutral";81 }82}8384function Figure({ label, children, note }: { label: ReactNode; children: ReactNode; note?: ReactNode }) {85 return (86 <div className="min-w-0 px-3 py-3 sm:px-4 sm:first:pl-0">87 <div className="label-caps mb-1 truncate">{label}</div>88 <div className="min-w-0">{children}</div>89 {note && <div className="mt-1 text-[11px] leading-tight text-ink-3">{note}</div>}90 </div>91 );92}9394const PROJECT_CLASS_LABEL: Record<string, string> = {95 NEW_BUILD: "New build",96 EXPANSION: "Expansion",97 CONSTRUCTION_START: "Construction start",98 PERMIT: "Permit",99 LAND_ACQUISITION: "Land acquisition",100 GRID_CONNECTION: "Grid connection",101 POWER_AGREEMENT: "Power agreement",102 FINANCING: "Financing",103 ACQUISITION: "Acquisition",104 PARTNERSHIP: "Partnership",105 CUSTOMER_AGREEMENT: "Customer agreement",106};107108export default async function ProjectPage({ params }: Props) {109 const { slug } = await params;110 const r = await getProject(slug);111 if (!r.ok && r.status === 404) notFound();112 if (!r.ok) {113 return (114 <Page className="py-6">115 <OfflineNotice detail={r.error} className="mb-4" />116 <ErrorState />117 </Page>118 );119 }120 const p = r.data;121 const base = routes.project(p.slug);122 const place = placeLine(p);123 const hasGeo = p.lat !== null && p.lng !== null;124 const precision = p.geoPrecision ?? (hasGeo ? "city" : "unknown");125 const claims = p.claims ?? [];126 const history = p.history ?? [];127 const mwSite = siteScoped(p.capacityScope);128 const usdSite = siteScoped(p.investmentScope);129 const dq = p.dataQuality;130131 const timeline: TimelineEntry[] = [...p.timeline]132 .sort((a, b) => partialDateSortKey(a.date.length > 10 ? a.date.slice(0, 10) : a.date) - partialDateSortKey(b.date.length > 10 ? b.date.slice(0, 10) : b.date))133 .map((t) => ({ date: t.date, title: humanType(t.type), description: t.description, meta: t.sourceName ? <span>{t.sourceName}</span> : undefined, href: t.url, tone: toneFor(t.type) }));134 for (const s of p.statusHistory) timeline.push({ date: s.date, title: `${s.from ? `${STATUS_LABEL[s.from]} → ` : ""}${STATUS_LABEL[s.to]}`, href: s.url, tone: s.to === "under_construction" ? "warn" : s.to === "operational" ? "ok" : s.to === "cancelled" || s.to === "delayed" ? "danger" : "neutral" });135 if (p.expectedOpening) timeline.push({ date: p.expectedOpening, title: <span className="text-ink-2">Expected opening</span>, description: p.phaseCount ? `phase 1 of ${fmtInt(p.phaseCount)} — expected, as published by the source` : "expected, as published by the source", tone: "neutral" });136 timeline.sort((a, b) => partialDateSortKey(a.date.slice(0, 10)) - partialDateSortKey(b.date.slice(0, 10)));137138 const sections = [139 { id: "lifecycle", label: "Lifecycle" },140 { id: "figures", label: "Figures & claims" },141 { id: "timeline", label: "Timeline", count: timeline.length },142 { id: "about", label: "About", count: p.description || p.sourceUrl || p.facility || p.campus ? 1 : 0 },143 { id: "location", label: "Location" },144 { id: "nearby", label: "Nearby", count: hasGeo ? 1 : 0 },145 { id: "related", label: "Related events", count: p.relatedEvents.length + p.events.length },146 { id: "provenance", label: "Source evidence", count: p.provenance.length },147 { id: "quality", label: "Data quality" },148 ];149150 const jsonLd = compactJsonLd({151 "@context": "https://schema.org",152 "@type": "Project",153 name: p.name,154 description: p.description ?? undefined,155 url: absoluteUrl(base),156 sameAs: p.sourceUrl ?? undefined,157 foundingDate: p.announcedOn ?? undefined,158 parentOrganization: p.operator ? { "@type": "Organization", name: p.operator.name, url: absoluteUrl(routes.operator(p.operator.slug)) } : undefined,159 location: place !== "—" || hasGeo ? compactJsonLd({ "@type": "Place", name: place !== "—" ? place : undefined, address: p.countryIso2 ? { "@type": "PostalAddress", addressLocality: p.city ?? undefined, addressRegion: p.regionName ?? undefined, addressCountry: p.countryIso2 } : undefined, geo: hasGeo && precision !== "unknown" ? { "@type": "GeoCoordinates", latitude: p.lat, longitude: p.lng } : undefined }) : undefined,160 additionalProperty: [161 { "@type": "PropertyValue", name: "status", value: STATUS_LABEL[p.status] },162 p.plannedMw !== null && mwSite ? { "@type": "PropertyValue", name: "Planned capacity", value: p.plannedMw, unitText: "MW" } : null,163 { "@type": "PropertyValue", name: "geoPrecision", value: PRECISION_LABEL[precision] },164 ].filter(Boolean),165 });166167 const mwSplit = splitMw(p.plannedMw);168169 return (170 <EvidenceProvider data={{ entityName: p.name, entityKind: "project", claims, provenance: p.provenance, history }}>171 <Page>172 <Breadcrumbs items={[{ name: "Projects", href: routes.projects() }, { name: p.name, href: base }]} className="pt-3" />173 <JsonLd data={jsonLd} />174175 <header className="py-5 sm:py-7">176 <div className="label-caps mb-1.5 flex flex-wrap items-center gap-x-2 gap-y-1">177 <span>Project</span>178 {p.projectClass && PROJECT_CLASS_LABEL[p.projectClass] && (179 <>180 <span aria-hidden className="text-ink-4">·</span>181 <span>{PROJECT_CLASS_LABEL[p.projectClass]}</span>182 </>183 )}184 {p.operator && (185 <>186 <span aria-hidden className="text-ink-4">·</span>187 <Link href={routes.operator(p.operator.slug)} className="normal-case tracking-normal text-[12.5px] font-medium text-ink-2 hover:text-accent hover:underline">188 {p.operator.name}189 </Link>190 </>191 )}192 </div>193 <div className="flex flex-wrap items-start justify-between gap-2">194 <h1 className="text-[22px] font-semibold leading-tight tracking-[-0.02em] text-ink sm:text-[28px]">{p.name}</h1>195 <WatchButton entityType="project" slug={p.slug} entityId={p.id} name={p.name} />196 </div>197 <div className="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-[13px] text-ink-2">198 {!p.operator && <span className="text-ink-3">Operator not published</span>}199 {p.developer && p.developer.id !== p.operator?.id && (200 <span>201 developed by{" "}202 <Link href={routes.operator(p.developer.slug)} className="text-ink hover:text-accent hover:underline">203 {p.developer.name}204 </Link>205 </span>206 )}207 {p.tenant && (208 <span>209 for{" "}210 <Link href={routes.operator(p.tenant.slug)} className="text-ink hover:text-accent hover:underline">211 {p.tenant.name}212 </Link>213 </span>214 )}215 {(place !== "—" || p.metro) && (216 <span className="inline-flex items-center gap-1.5">217 <CountryFlag iso2={p.countryIso2} size={12} />218 {p.metro && (219 <Link href={routes.market(p.metro.slug)} className="hover:text-ink hover:underline">220 {p.metro.name} market221 </Link>222 )}223 {p.metro && place !== "—" && <span className="text-ink-4">·</span>}224 {place !== "—" && <span>{place}</span>}225 </span>226 )}227 </div>228 <div className="mt-3 flex flex-wrap items-center gap-1.5">229 {p.isAi && <AiMark evidence={p.aiEvidence} size="sm" />}230 {p.evidenceLevel && (231 <Badge tone={p.evidenceLevel === "strong" ? "ok" : p.evidenceLevel === "weak" ? "warn" : "outline"} size="sm" title="Creation evidence: name or operator + location + development verb">232 {p.evidenceLevel} evidence233 </Badge>234 )}235 {p.phaseCount !== null && (236 <Badge tone="outline" size="sm">237 {fmtInt(p.phaseCount)} phases238 </Badge>239 )}240 </div>241 </header>242243 {/* header figures */}244 <div className="grid grid-cols-2 border-y border-hair [&>*]:min-w-0 max-sm:[&>*:nth-child(even)]:border-l max-sm:[&>*:nth-child(n+3)]:border-t max-sm:[&>*:nth-child(odd)]:pl-0 max-sm:[&>*]:border-hair sm:grid-cols-3 sm:[&>*:nth-child(n+4)]:border-t sm:[&>*:nth-child(3n+1)]:pl-0 sm:[&>*:not(:nth-child(3n+1))]:border-l sm:[&>*]:border-hair lg:grid-cols-6 lg:[&>*:nth-child(n+4)]:border-t-0 lg:[&>*:nth-child(3n+1)]:pl-4 lg:[&>*:first-child]:pl-0 lg:[&>*+*]:border-l">245 <Figure label="Stage">246 <Evidence field="status" display={STATUS_LABEL[p.status]}>247 <StatusBadge status={p.status} />248 </Evidence>249 </Figure>250 <Figure label="Planned capacity">251 {p.plannedMw !== null && mwSite && mwSplit ? (252 <Evidence field="plannedMw" fields={["plannedPowerMw", "planned_power_mw", "phase_mw"]} display={`≈ ${fmtMw(p.plannedMw)}`} block>253 <div className="flex items-baseline gap-1 leading-none">254 <EstimateMark className="text-[14px]" />255 <span className="figure text-[22px] font-medium tracking-[-0.03em] text-ink sm:text-[26px]">{mwSplit.value}</span>256 <span className="text-[12px] font-medium text-ink-3">{mwSplit.unit}</span>257 </div>258 <div className="mt-1 flex flex-wrap items-center gap-1.5">259 <SemanticsLabel semantics={p.capacitySemantics ?? "planned_power_mw"} />260 {p.capacityScope && <ScopeBadge scope={p.capacityScope} />}261 </div>262 </Evidence>263 ) : (264 <Evidence field="plannedMw" fields={["plannedPowerMw", "planned_power_mw", "phase_mw"]} display="—" block>265 <div className="figure text-[22px] font-medium leading-none tracking-[-0.03em] text-ink-4 sm:text-[26px]">—</div>266 <div className="mt-1 text-[11px] leading-tight text-ink-3">{p.plannedMw !== null ? `${fmtMw(p.plannedMw)} is a ${p.capacityScope}-level figure — not this site's (see claims)` : "Not published by any source"}</div>267 </Evidence>268 )}269 </Figure>270 <Figure label="Investment">271 {p.investmentUsd !== null && usdSite ? (272 <Evidence field="investmentUsd" fields={["investment_usd"]} display={fmtUsd(p.investmentUsd)} block>273 <div className="figure text-[22px] font-medium leading-none tracking-[-0.03em] text-ink sm:text-[26px]">{fmtUsd(p.investmentUsd)}</div>274 <div className="mt-1 flex flex-wrap items-center gap-1.5">275 <SemanticsLabel semantics={p.investmentSemantics ?? "project_investment_usd"} />276 {p.investmentScope && <ScopeBadge scope={p.investmentScope} />}277 {p.investmentCurrency && p.investmentCurrency !== "USD" && p.investmentOriginal && <span className="figure text-[11px] text-ink-3">{fmtInt(p.investmentOriginal)} {p.investmentCurrency}</span>}278 </div>279 </Evidence>280 ) : (281 <Evidence field="investmentUsd" fields={["investment_usd"]} display="—" block>282 <div className="figure text-[22px] font-medium leading-none tracking-[-0.03em] text-ink-4 sm:text-[26px]">—</div>283 <div className="mt-1 text-[11px] leading-tight text-ink-3">{p.investmentUsd !== null ? `${fmtUsd(p.investmentUsd)} is a ${p.investmentScope}-level figure — not this site's (see claims)` : "Not disclosed"}</div>284 </Evidence>285 )}286 </Figure>287 <Figure label="Expected opening" note={p.expectedOpening ? "as published" : "not published"}>288 <Evidence field="expectedOpening" fields={["expected_opening"]} display={p.expectedOpening ? fmtPartialDateSafe(p.expectedOpening) : "—"} block>289 <div className={`figure text-[18px] font-medium leading-none tracking-[-0.02em] ${p.expectedOpening ? "text-ink" : "text-ink-4"}`}>{p.expectedOpening ? fmtPartialDateSafe(p.expectedOpening) : "—"}</div>290 </Evidence>291 </Figure>292 <Figure label="Announced" note={p.announcedOn ? undefined : "date not published"}>293 <Evidence field="announcedOn" fields={["announced_on"]} display={p.announcedOn ? fmtDate(p.announcedOn) : "—"} block>294 <div className={`figure text-[18px] font-medium leading-none tracking-[-0.02em] ${p.announcedOn ? "text-ink" : "text-ink-4"}`}>{p.announcedOn ? fmtDate(p.announcedOn) : "—"}</div>295 </Evidence>296 </Figure>297 <Figure label="Confidence" note={`${fmtInt(dq.sourceCount)} ${dq.sourceCount === 1 ? "source" : "sources"} · ${fmtInt(dq.primarySourceCount)} primary`}>298 <ConfidenceBadge level={p.confidence} short={false} />299 </Figure>300 </div>301 <p className="mt-2 text-[11px] text-ink-4">Planned MW and investment are published figures (≈ never imputed). A company-wide or portfolio figure is never shown as this project's own; it is listed under “Figures not assigned to this site”. Click a figure for its evidence.</p>302303 <SectionNav items={sections} className="mt-4" />304305 <div className="flex flex-col gap-9 pt-6">306 <Section id="lifecycle" title="Lifecycle" className="scroll-mt-28" description="Rumored → proposed → announced → permitting → approved → construction → opened. Dates come from the announcement that moved the stage; delayed / cancelled are side branches.">307 <ProjectStages stages={p.stages} />308 <div className="mt-6">309 <div className="label-caps mb-2">Velocity</div>310 <ProjectVelocity v={p.velocityDays} />311 </div>312 </Section>313314 <Section id="figures" title="Figures & claims" className="scroll-mt-28" description="Every capacity and investment claim extracted for this project with its scope. Only site-scoped claims can become the project's figures.">315 <div className="flex flex-col gap-5">316 <div>317 <div className="label-caps mb-2">Capacity claims</div>318 <ClaimsTable claims={claims} kind="capacity" emptyNote={p.plannedMw !== null ? "No sentence-level capacity claim stored yet — the planned figure comes from the source fields listed under Source evidence." : "No source has published a capacity figure for this project."} />319 </div>320 <div>321 <div className="label-caps mb-2">Investment claims</div>322 <ClaimsTable claims={claims} kind="investment" emptyNote={p.investmentUsd !== null ? "No sentence-level investment claim stored yet — the figure comes from the source fields listed under Source evidence." : "No investment figure disclosed."} />323 </div>324 {((p.plannedMw !== null && !mwSite) || (p.investmentUsd !== null && !usdSite)) && (325 <div>326 <div className="mb-1.5 flex flex-wrap items-baseline gap-x-2">327 <h3 className="text-[13px] font-semibold text-ink">Figures not assigned to this site</h3>328 <span className="text-[11.5px] text-ink-3">scope is wider than the project — shown as context only</span>329 </div>330 <KeyValueList331 items={[332 { label: "Planned MW", value: p.plannedMw !== null && !mwSite ? <span className="inline-flex flex-wrap items-center gap-1.5"><span className="figure">≈ {fmtMw(p.plannedMw)}</span><ScopeBadge scope={p.capacityScope} /><SemanticsLabel semantics={p.capacitySemantics} /></span> : null },333 { label: "Investment", value: p.investmentUsd !== null && !usdSite ? <span className="inline-flex flex-wrap items-center gap-1.5"><span className="figure">{fmtUsd(p.investmentUsd)}</span><ScopeBadge scope={p.investmentScope} /><SemanticsLabel semantics={p.investmentSemantics} /></span> : null },334 ]}335 />336 </div>337 )}338 </div>339 </Section>340341 <Section id="timeline" title="Timeline" className="scroll-mt-28" description="Dated milestones and status transitions from the sources indexed; partial dates (year, quarter) are shown as published.">342 {timeline.length ? <TimelineList items={timeline} /> : <p className="text-[12.5px] text-ink-3">No dated milestone has been published for this project yet.</p>}343 </Section>344345 <Section id="about" title="About" className="scroll-mt-28">346 {p.description ? <p className="max-w-3xl text-[14px] leading-relaxed text-ink-2">{p.description}</p> : <p className="text-[12.5px] text-ink-3">No description published.</p>}347 <KeyValueList348 className="mt-3"349 columns={2}350 items={[351 { label: "Operator", value: p.operator ? <Link href={routes.operator(p.operator.slug)} className="hover:text-accent hover:underline">{p.operator.name}</Link> : <span className="text-[12.5px] text-ink-3">Not published</span>, keepEmpty: true },352 { label: "Developer", value: p.developer ? <Link href={routes.operator(p.developer.slug)} className="hover:text-accent hover:underline">{p.developer.name}</Link> : <span className="text-[12.5px] text-ink-3">Not published</span>, keepEmpty: true },353 { label: "Tenant", value: p.tenant ? <Link href={routes.operator(p.tenant.slug)} className="hover:text-accent hover:underline">{p.tenant.name}</Link> : null },354 { label: "Facility", value: p.facility ? <Link href={routes.facility(p.facility.slug)} className="hover:text-accent hover:underline">{p.facility.name}</Link> : null },355 { label: "Campus", value: p.campus ? <Link href={routes.facility(p.campus.slug)} className="hover:text-accent hover:underline">{p.campus.name}</Link> : null },356 { label: "Site", value: p.acreage !== null ? <span className="figure">{fmtInt(p.acreage)} acres</span> : null },357 { label: "Primary source", value: p.sourceUrl ? <ExtLink href={p.sourceUrl} /> : null },358 { label: "Last update", value: <span className="figure text-[12.5px] text-ink-2">{fmtDate(p.lastUpdate)}</span> },359 ]}360 />361 </Section>362363 <Section id="location" title="Location" className="scroll-mt-28" description={hasGeo ? `${PRECISION_LABEL[precision]} — the ring marks the area published by the sources, not a parcel.` : "Location as published; no coordinates yet."} aside={<PrecisionBadge precision={precision} short={false} size="xs" />}>364 <div className="grid gap-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)]">365 <KeyValueList366 items={[367 { label: "City", value: p.city },368 { label: "Region", value: p.regionName },369 { label: "Country", value: p.countryIso2 ? <span className="inline-flex items-center gap-1.5"><CountryFlag iso2={p.countryIso2} size={12} />{countryName(p.countryIso2)}</span> : null },370 { label: "Market", value: p.metro ? <Link href={routes.market(p.metro.slug)} className="hover:text-accent hover:underline">{p.metro.name}</Link> : null },371 { label: "Coordinates", value: hasGeo ? <Evidence field="lat" fields={["lng", "geo", "coordinates", "geoPrecision"]}><span className="figure">{p.lat!.toFixed(precision === "exact" || precision === "parcel" ? 4 : 2)}, {p.lng!.toFixed(precision === "exact" || precision === "parcel" ? 4 : 2)}</span></Evidence> : <span className="text-[12.5px] text-ink-3">Not published</span>, keepEmpty: true },372 { label: "Precision", value: <WithProvenance items={p.provenance} fields={["geoPrecision", "geo_precision"]}><PrecisionBadge precision={precision} short={false} size="xs" /></WithProvenance>, keepEmpty: true },373 ]}374 />375 {hasGeo ? <EntityMap center={{ lat: p.lat!, lng: p.lng! }} precision={precision} filters={p.countryIso2 ? { country: p.countryIso2 } : undefined} className="h-[220px] sm:h-[280px]" /> : <div className="flex min-h-[140px] items-center justify-center rounded-sm border border-dashed border-hair-2 text-[12.5px] text-ink-3">No coordinates published — not drawn on the map.</div>}376 </div>377 </Section>378379 <Section id="nearby" title="Nearby infrastructure" className="scroll-mt-28" description="Great-circle distance from the published (often city-level) point. Proximity never implies a connection.">380 {hasGeo ? <NearbyInfrastructurePanel initial={p.nearbyInfrastructure} center={{ lat: p.lat!, lng: p.lng! }} /> : <p className="rounded-sm border border-dashed border-hair-2 px-3 py-2.5 text-[12.5px] text-ink-3">Nearby infrastructure needs a published location for this project.</p>}381 </Section>382383 <Section id="related" title="Related events" className="scroll-mt-28" description="Events attached to this project — announcements, power agreements, financing, status changes — deduplicated across sources.">384 <EventList events={[...p.relatedEvents, ...p.events.filter((e) => !p.relatedEvents.some((x) => x.id === e.id))]} dense emptyTitle="No related event yet" emptyDescription="Announcements, agreements and status changes will list here as connectors detect them." />385 </Section>386387 <Section id="provenance" title="Source evidence" className="scroll-mt-28" description="Every field with its source, observation dates, confidence and extraction method. “Shown” marks the winner backing the displayed value.">388 <ProvenanceTable items={p.provenance} />389 {p.sourceHistory.length > 0 && (390 <details className="mt-4">391 <summary className="cursor-pointer text-[12.5px] font-medium text-ink-2 hover:text-ink">392 Source observation log <span className="figure text-ink-3">{fmtInt(p.sourceHistory.length)}</span>393 </summary>394 <div className="mt-3">395 <SourceHistory items={p.sourceHistory} />396 </div>397 </details>398 )}399 <p className="mt-2 text-[11.5px] text-ink-4">400 {CONFIDENCE_LABEL[p.confidence]} overall.{" "}401 <Link href={`${routes.methodology()}#confidence`} className="underline decoration-hair-2 hover:text-ink">402 How confidence is computed403 </Link>404 </p>405 </Section>406407 <Section id="quality" title="Data quality" className="scroll-mt-28">408 <DataQualityPanel q={dq} />409 </Section>410 </div>411412 <SourcesFooter sources={r.sources} updatedAt={p.lastUpdate} />413 </Page>414 </EvidenceProvider>415 );416}417